package net.avcompris.jaxen.yaml;

import static com.avcompris.util.ExceptionUtils.nonNullArgument;
import static net.avcompris.jaxen.yaml.YamlNodes.isEmptyAttributeValue;
import static net.avcompris.jaxen.yaml.YamlNodes.isSimpleValue;
import static net.avcompris.jaxen.yaml.YamlXPath.NamespaceURI.XMLNSURI_AVC_BINDING_YAML;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import net.avcompris.binding.BindConfiguration;
import net.avcompris.binding.yaml.impl.YamlContent;
import net.avcompris.jaxen.yaml.YamlXPath.NamespaceURI;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.avcompris.common.annotation.Nullable;
import com.avcompris.lang.NotImplementedException;
import com.google.common.collect.Iterables;

/**
 * container for a node's name and a its content.
 * 
 * @author David Andrianavalontsalama
 */
final class YamlNode implements Serializable, YamlWithProperties, YamlContent {

	/**
	 * for serialization.
	 */
	private static final long serialVersionUID = -2192174530245000903L;

	private YamlNode(final Object parent, @Nullable NamespaceURI namespaceUri,
			final String name, @Nullable final Object content,
			@Nullable final Object subContent,
			final BindConfiguration configuration) {

		this.parent = nonNullArgument(parent, "parent");
		this.namespaceUri = namespaceUri;
		this.name = nonNullArgument(name, "name");
		this.content = content;
		this.subContent = subContent;
		this.configuration = nonNullArgument(configuration, "configuration");
	}

	public YamlNode(final Object parent, final String name,
			@Nullable final Object content,
			final BindConfiguration configuration) {

		this(parent, null, name, content, null, configuration);
	}

	public YamlNode(final YamlNode parent, final String name,
			@Nullable final Object content) {

		this(parent, null, name, content);
	}

	private YamlNode(final YamlNode parent,
			@Nullable final NamespaceURI namespaceUri, final String name,
			@Nullable final Object content) {

		this(parent, namespaceUri, name, content, null);
	}

	private YamlNode(final YamlNode parent,
			@Nullable final NamespaceURI namespaceUri, final String name,
			@Nullable final Object content, @Nullable final Object subContent) {

		this(parent, namespaceUri, name, content, subContent, nonNullArgument(
				parent, "parent").configuration);
	}

	private final Object parent;
	private final String name;
	private final Object content;
	@Nullable
	private final Object subContent;
	private final BindConfiguration configuration;

	public Object getParent() {

		return parent;
	}

	public String getName() {

		return name;
	}

	@Nullable
	public String getNamespaceUri() {

		return namespaceUri == null ? null : namespaceUri.getURI();
	}

	private final NamespaceURI namespaceUri;

	public Object getContent() {

		return content;
	}

	@Override
	public String getTextContent() {

		return getStringValue();
	}

	public String getStringValue() {

		if (isSimpleValue(content)) {

			return content.toString();
		}

		if (subContent != null && isSimpleValue(subContent)) {

			return subContent.toString();
		}

		log.error("getStringValue(): name: " + name);
		log.error("getStringValue(): content.type: "
				+ content.getClass().getName());

		throw new NotImplementedException("content: " + content);
	}

	@Override
	public int hashCode() {

		return name.hashCode() + ((content == null) ? 0 : content.hashCode());
	}

	@Override
	public boolean equals(@Nullable final Object o) {

		if (o == null) {

			return false;
		}

		if (!YamlNode.class.equals(o.getClass())) {

			return false;
		}

		final YamlNode n = (YamlNode) o;

		if (!name.equals(n.name)) {

			return false;
		}

		if (content == null) {

			return n.content == null;
		}

		return content.equals(n.content);
	}

	@Override
	public String toString() {

		return YamlNode.class.getSimpleName() + "::" + name + ": " + content
				+ " (sub:" + subContent + ")";
	}

	@Override
	public String[] getPropertyNames() {

		if (propertyNames != null) {

			return propertyNames;
		}

		calcPropertyNames();

		return propertyNames;
	}

	private String[] propertyNames = null;

	private Collection<YamlNode> propertyKeyObjects = null;

	/**
	 * cache property names, because we are not certain that the order will be
	 * preserved if we calculate them twice.
	 */
	private synchronized void calcPropertyNames() {

		if (log.isDebugEnabled()) {
			log.debug("calcPropertyNames()");
		}

		if (propertyNames != null) {

			if (log.isDebugEnabled()) {
				log.debug("  => calcPropertyNames(): propertyNames != null");
			}

			return;
		}

		final Map<?, ?> map;

		if (Map.class.isInstance(content)) {

			map = (Map<?, ?>) content;

		} else {

			if (log.isDebugEnabled()) {
				log.debug("  => calcPropertyNames(): content.class = "
						+ content.getClass().getName());
			}

			return;
		}

		final Collection<String> propertyNames = new ArrayList<String>(); // ordered

		this.propertyKeyObjects = new ArrayList<YamlNode>(); // ordered

		final List<Object> orderedKeys = new ArrayList<Object>();

		final Set<String> stringKeys = new TreeSet<String>();

		for (final Object key : map.keySet()) {

			if (String.class.isInstance(key)) {

				stringKeys.add((String) key);

			} else {

				orderedKeys.add(key);
			}
		}

		orderedKeys.addAll(0, stringKeys);

		for (final Object key : orderedKeys) {

			if (!String.class.isInstance(key)) {

				if (log.isDebugEnabled()) {
					log.debug("Key is not String: " + key.getClass().getName()
							+ ": " + key);
				}

				// throw new RuntimeException("Key is not String: " + key);

				final Map<?, ?> keyMap;

				if (Map.class.isInstance(key)) {

					keyMap = (Map<?, ?>) key;

				} else if (!List.class.isInstance(key)) {

					throw new NotImplementedException(
							"key.type != map && != list");

					// continue;

				} else {

					final List<?> list = (List<?>) key;

					final boolean isUniqueItemMap;
					
					if (list.size() != 1) {
						
						isUniqueItemMap = false;
						
					} else {
						
						final Object item = list.iterator().next();

						isUniqueItemMap  = Map.class.isInstance(item);
					}
					
					if (!isUniqueItemMap) {

						// throw new
						// NotImplementedException("list.size() != 1");

						final Map<String, List<?>> listMap = new HashMap<String, List<?>>();

						/*
						 * for (final Object item : (List<?>) value) {
						 * 
						 * final YamlNode child = new YamlNode(this,
						 * propertyName, item);
						 * 
						 * children.add(child); }
						 */

						listMap.put("key", list);
						//final Map<String, String> totoMap = new HashMap<String, String>();
						//totoMap.put("key", "toto");
						keyMap = 
								listMap;
								//totoMap;
						// listMap;

						// continue;

					} else {

						final Object item = list.iterator().next();

					//	if (!Map.class.isInstance(item)) {

						//	throw new NotImplementedException(
							//		"item.type != map");

							// continue;
						//}

						keyMap = (Map<?, ?>) item;
					}
				}

				final YamlNode propertyKeyObject = new YamlNode(this,
						XMLNSURI_AVC_BINDING_YAML, "key", keyMap, map.get(key));

				propertyKeyObject.calcPropertyNames();

				propertyKeyObjects.add(propertyKeyObject);

				continue;
			}

			final String propertyName = (String) key;

			propertyNames.add(propertyName);
		}

		this.propertyNames = Iterables.toArray(propertyNames, String.class); // final
																				// atomic
																				// assignation,
																				// thread-safe
	}

	@Override
	public Object getObjectProperty(final String name) {

		nonNullArgument(name, "name");

		if (!Map.class.isInstance(content)) {

			return null;
		}

		final Map<?, ?> map = (Map<?, ?>) content;

		return map.get(name);
	}

	public YamlNode[] getChildren() {

		if (log.isDebugEnabled()) {
			log.debug("getChildren()");
		}

		if (children != null) {

			return children;
		}

		calcChildren();

		if (log.isDebugEnabled()) {
			log.debug("  => getChildren(): " + children.length);
		}

		return children;
	}

	private YamlNode[] children = null;

	private static final Log log = LogFactory.getLog(YamlNode.class);

	private void calcChildren() {

		if (log.isDebugEnabled()) {
			log.debug("calcChildren()");
		}

		if (children != null) {

			if (log.isDebugEnabled()) {
				log.debug("  => calcChildren(): children != null");
			}

			return;
		}

		final String[] propertyNames = getPropertyNames();

		if (propertyNames == null) {

			if (log.isDebugEnabled()) {
				log.debug("  => calcChildren(): propertyNames == null");
			}

			children = new YamlNode[0];

			return;
		}

		final List<YamlNode> children = new ArrayList<YamlNode>();

		if (log.isDebugEnabled()) {
			log.debug("  => calcChildren(): propertyNames.length = "
					+ propertyNames.length);
		}

		for (int i = 0; i < propertyNames.length; ++i) {

			final String propertyName = propertyNames[i];

			final Object value = getObjectProperty(propertyName);

			if (isEmptyAttributeValue(value)) {

				if (configuration.isNodesElementsEverywhere()
						&& configuration.isNodesEmptyAttributes()) {

					final YamlNode child = new YamlNode(this, propertyName, "");

					children.add(child);

				} else {

					continue;
				}

			} else if (isSimpleValue(value)) {

				if (configuration.isNodesElementsEverywhere()) {

					final YamlNode child = new YamlNode(this, propertyName,
							value.toString());

					children.add(child);

				} else {

					continue;
				}

			} else if (List.class.isInstance(value)) {

				for (final Object item : (List<?>) value) {

					final YamlNode child = new YamlNode(this, propertyName,
							item);

					children.add(child);
				}

			} else {

				final YamlNode child = new YamlNode(this, propertyName, value);

				children.add(child);
			}
		}

		children.addAll(propertyKeyObjects);

		if (subContent != null) {

			if (subContent instanceof YamlNode) {

				children.add((YamlNode) subContent);

			} else {

				children.add(new YamlNode(this, XMLNSURI_AVC_BINDING_YAML,
						"value", subContent));
			}
		}

		this.children = Iterables.toArray(children, YamlNode.class); // final
																		// atomic
																		// assignation,
																		// thread-safe
	}
	// public Iterator<YamlNode> getChildIterator() {
	//
	// final YamlNode[] children = getChildren();
	//
	// return Arrays.asList(children).iterator();
	// }
}
